package com.thghh.table;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

import javax.swing.table.AbstractTableModel;

/**
 * The mapping between Java Bean and table column is established by using java
 * introspection mechanism, and the fields of java bean are resolved as the
 * columns of table
 * <p>
 * Generally there are two cases, one is subclass inheritance, the other is
 * direct use<blockquote>
 *
 * <pre>
 *     Situation 1
 *     class A {
 *         Integer id;
 *         String name;
 *     }
 *
 *     class MyTableModel extends ElegantTableModel<A> {
 *
 *     }
 *
 *     A a = new A(1, "name");
 *     List<A> dataList = new ArrayList<>();
 *     dataList.add(a);
 *     ElegantTableModel myTableModel = new MyTableModel();
 *     myTableModel.setDataList(dataList);
 * </pre>
 *
 * </blockquote>
 * <p>
 *
 * <pre>
 *     Situation 2
 *     class A {
 *         Integer id;
 *         String name;
 *     }
 *
 *     A a = new A(1, "name");
 *     List<A> dataList = new ArrayList<>();
 *     dataList.add(a);
 *     ElegantTableModel myTableModel = new ElegantTableModel(A.class, dataList);
 * </pre>
 *
 * </blockquote>
 *
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/9/11 10:59
 */
public class ElegantTableModel<T> extends AbstractTableModel {

    private static final long serialVersionUID = 2020L;
    public static final String Update = "Update";
    public static final String UpdateLine = "UpdateLine";
    public static final String Delete = "Delete";
    public static final String Insert = "Insert";
    /** Specifies all columns in a row or rows. */
    public static final int ALL_COLUMNS = -1;

    protected Class<?> targetClass;
    protected List<T> dataList;
    private BeanColumnAnnotationProcessor beanColumnProcessor;
    private PropertyChangeSupport changeSupport;

    protected ElegantTableModel() {
        targetClass = getGenericSuperclass();
        beanColumnProcessor = new BeanColumnAnnotationProcessor(targetClass);
    }

    /**
     * When class is parsed, the fields with {@ link BeanColumn} annotation will
     * become columns in the table
     *
     * @param clazz
     *            Java bean class
     * @param dataList
     *            data list
     */
    public ElegantTableModel(Class<?> clazz, List<T> dataList) {
        this(clazz);
        setDataList(dataList);
    }

    /**
     * When class is parsed, the fields with {@ link BeanColumn} annotation will
     * become columns in the table
     *
     * @param clazz
     *            Java bean class
     */
    public ElegantTableModel(Class<?> clazz) {
        targetClass = clazz;
        beanColumnProcessor = new BeanColumnAnnotationProcessor(targetClass);
    }

    /**
     * Getting the real type of a generic from a generic
     *
     * @return Class
     */
    private Class<?> getGenericSuperclass() {
        Class<?> clazz = getClass();
        while (clazz != Object.class) {
            Type t = clazz.getGenericSuperclass();
            if (t instanceof ParameterizedType) {
                Type[] args = ((ParameterizedType) t).getActualTypeArguments();
                if (args[0] instanceof Class) {
                    return (Class<?>) args[0];
                }
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    /**
     * Set data list
     *
     * @param dataList
     */
    public void setDataList(List<T> dataList) {
        if (dataList == null) {
            throw new CheckException("data list is null.");
        }
        if (this.dataList == dataList) {
            return;
        }
        if (this.dataList != null && !this.dataList.isEmpty()) {
            this.dataList.clear();
            fireTableRowsDeleted(0, dataList.size());
        }
        List<T> oldList = this.dataList;
        this.dataList = dataList;
        firePropertyChange(Insert, oldList, dataList);
        fireTableRowsInserted(0, dataList.size());
    }

    /**
     * Reset data list
     *
     * @param dataList
     */
    public void resetDataList(List<T> dataList) {
        if (dataList == null) {
            throw new CheckException("data list is null.");
        }
        List<T> oldList = this.dataList;
        this.dataList = dataList;
        firePropertyChange(Insert, oldList, dataList);
        fireTableRowsInserted(0, dataList.size());
    }

    /**
     * Clean data model
     *
     * @author Zhikang.Peng
     * @date 2021-05-08 11:36:46
     * @since 1.0
     */
    public void cleanData() {
        if (this.dataList != null && !this.dataList.isEmpty()) {
            int size = dataList.size();
            this.dataList.clear();
            fireTableRowsInserted(0, size);
        }
    }

    public List<T> getDataList() {
        return dataList;
    }

    @Override
    public int getRowCount() {
        return dataList == null ? 0 : dataList.size();
    }

    @Override
    public int getColumnCount() {
        return beanColumnProcessor.getColumnCount();
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Object obj;
        try {
            T data = dataList.get(rowIndex);
            obj = beanColumnProcessor.getPropertyValue(data, columnIndex);
        } catch (Exception e) {
            throw new CheckException(e);
        }
        return obj;
    }

    @Override
    public String getColumnName(int column) {
        return beanColumnProcessor.getBeanColumn(column).name();
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return beanColumnProcessor.getPropertyDescriptor(columnIndex).getPropertyType();
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return beanColumnProcessor.getBeanColumn(columnIndex).editable();
    }

    public boolean isCellEditable(int columnIndex) {
        return beanColumnProcessor.getBeanColumn(columnIndex).editable();
    }

    public boolean isCellEditable(String name) {
        int index = getColumnIndex(name);
        return index == -1 ? false : isCellEditable(index);
    }

    public int getColumnIndex(String name) {
        for (int i = 0; i < beanColumnProcessor.getColumnCount(); i++) {
            BeanColumn beanColumn = beanColumnProcessor.getBeanColumn(i);
            if (beanColumn.name().equals(name)) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        try {
            T data = dataList.get(rowIndex);
            Object oldValue = this.getValueAt(rowIndex, columnIndex);
            beanColumnProcessor.setPropertyValue(data, columnIndex, aValue);
            firePropertyChange(Update, oldValue, aValue);
            fireTableCellUpdated(rowIndex, columnIndex);
        } catch (Exception e) {
            throw new CheckException(e);
        }
    }

    /**
     * add data line
     *
     * @param data
     * @return
     */
    public boolean addRow(T data) {
        int oldSize = dataList.size();
        boolean b = dataList.add(data);
        firePropertyChange(Insert, null, data);
        fireTableRowsInserted(oldSize, dataList.size());
        return b;
    }

    /**
     * Add multi row data
     *
     * @param datas
     * @return
     */
    public boolean addRow(List<T> datas) {
        if (datas == null) {
            throw new CheckException("data list is null");
        }
        int oldSize = dataList.size();
        boolean b = dataList.addAll(datas);
        firePropertyChange(Insert, null, datas);
        fireTableRowsInserted(oldSize, dataList.size());
        return b;
    }

    /**
     * Delete data rows according to row index
     *
     * @param rowIndex
     * @return
     */
    public T deleteRow(int rowIndex) {
        rangeCheck(rowIndex);
        T d = dataList.remove(rowIndex);
        firePropertyChange(Delete, d, null);
        fireTableRowsDeleted(rowIndex, rowIndex);
        return d;
    }

    /**
     * Find the data from the data list and delete it
     *
     * @param data
     * @return
     */
    public T deleteRow(T data) {
        int index = dataList.indexOf(data);
        return deleteRow(index);
    }

    public T getData(int rowIndex) {
        rangeCheck(rowIndex);
        return dataList.get(rowIndex);
    }

    public T setData(int rowIndex, T data) {
        rangeCheck(rowIndex);
        T oldData = dataList.set(rowIndex, data);
        firePropertyChange(UpdateLine, oldData, data);
        fireTableRowsUpdated(rowIndex, rowIndex);
        return oldData;
    }

    /**
     * Constructs an IndexOutOfBoundsException detail message. Of the many
     * possible refactorings of the error handling code, this "outlining"
     * performs best with both server and client VMs.
     */
    private String outOfBoundsMsg(int index) {
        return "Index: " + index + ", Size: " + getRowCount();
    }

    /**
     * A version of rangeCheck used by add and addAll.
     */
    private void rangeCheck(int index) {
        if (index > getRowCount() || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /*
     * In some cases using "this" as an object to synchronize by can lead to a
     * deadlock if client code also uses synchronization by a component object.
     * For every such situation revealed we should consider possibility of
     * replacing "this" with the package private objectLock object introduced
     * below. So far there're 3 issues known: - CR 6708322 (the getName/setName
     * methods); - CR 6608764 (the PropertyChangeListener machinery); - CR
     * 7108598 (the
     * Container.paint/KeyboardFocusManager.clearMostRecentFocusOwner methods).
     *
     * Note: this field is considered final, though readObject() prohibits
     * initializing final fields.
     */
    private transient Object objectLock = new Object();

    Object getObjectLock() {
        return objectLock;
    }

    /**
     * Adds a PropertyChangeListener to the listener list. The listener is
     * registered for all bound properties of this class, including the Note
     * that if this <code>Component</code> is inheriting a bound property, then
     * no event will be fired in response to a change in the inherited property.
     * <p>
     * If <code>listener</code> is <code>null</code>, no exception is thrown and
     * no action is performed.
     *
     * @param listener
     *            the property change listener to be added
     *
     * @see #removePropertyChangeListener
     * @see #getPropertyChangeListeners
     * @see #addPropertyChangeListener(java.lang.String,
     *      java.beans.PropertyChangeListener)
     */
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        synchronized (getObjectLock()) {
            if (listener == null) {
                return;
            }
            if (changeSupport == null) {
                changeSupport = new PropertyChangeSupport(this);
            }
            changeSupport.addPropertyChangeListener(listener);
        }
    }

    /**
     * Removes a PropertyChangeListener from the listener list. This method
     * should be used to remove PropertyChangeListeners that were registered for
     * all bound properties of this class.
     * <p>
     * If listener is null, no exception is thrown and no action is performed.
     *
     * @param listener
     *            the PropertyChangeListener to be removed
     *
     * @see #addPropertyChangeListener
     * @see #getPropertyChangeListeners
     * @see #removePropertyChangeListener(java.lang.String,java.beans.PropertyChangeListener)
     */
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        synchronized (getObjectLock()) {
            if (listener == null || changeSupport == null) {
                return;
            }
            changeSupport.removePropertyChangeListener(listener);
        }
    }

    /**
     * Returns an array of all the property change listeners registered on this
     * component.
     *
     * @return all of this component's <code>PropertyChangeListener</code>s or
     *         an empty array if no property change listeners are currently
     *         registered
     *
     * @see #addPropertyChangeListener
     * @see #removePropertyChangeListener
     * @see #getPropertyChangeListeners(java.lang.String)
     * @see java.beans.PropertyChangeSupport#getPropertyChangeListeners
     * @since 1.4
     */
    public PropertyChangeListener[] getPropertyChangeListeners() {
        synchronized (getObjectLock()) {
            if (changeSupport == null) {
                return new PropertyChangeListener[0];
            }
            return changeSupport.getPropertyChangeListeners();
        }
    }

    /**
     * Adds a PropertyChangeListener to the listener list for a specific
     * property. The specified property may be user-defined, or one of the Note
     * that if this <code>Component</code> is inheriting a bound property, then
     * no event will be fired in response to a change in the inherited property.
     * <p>
     * If <code>propertyName</code> or <code>listener</code> is
     * <code>null</code>, no exception is thrown and no action is taken.
     *
     * @param propertyName
     *            one of the property names listed above
     * @param listener
     *            the property change listener to be added
     *
     * @see #removePropertyChangeListener(java.lang.String,
     *      java.beans.PropertyChangeListener)
     * @see #getPropertyChangeListeners(java.lang.String)
     * @see #addPropertyChangeListener(java.lang.String,
     *      java.beans.PropertyChangeListener)
     */
    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        synchronized (getObjectLock()) {
            if (listener == null) {
                return;
            }
            if (changeSupport == null) {
                changeSupport = new PropertyChangeSupport(this);
            }
            changeSupport.addPropertyChangeListener(propertyName, listener);
        }
    }

    /**
     * Removes a <code>PropertyChangeListener</code> from the listener list for
     * a specific property. This method should be used to remove
     * <code>PropertyChangeListener</code>s that were registered for a specific
     * bound property.
     * <p>
     * If <code>propertyName</code> or <code>listener</code> is
     * <code>null</code>, no exception is thrown and no action is taken.
     *
     * @param propertyName
     *            a valid property name
     * @param listener
     *            the PropertyChangeListener to be removed
     *
     * @see #addPropertyChangeListener(java.lang.String,
     *      java.beans.PropertyChangeListener)
     * @see #getPropertyChangeListeners(java.lang.String)
     * @see #removePropertyChangeListener(java.beans.PropertyChangeListener)
     */
    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        synchronized (getObjectLock()) {
            if (listener == null || changeSupport == null) {
                return;
            }
            changeSupport.removePropertyChangeListener(propertyName, listener);
        }
    }

    /**
     * Returns an array of all the listeners which have been associated with the
     * named property.
     *
     * @return all of the <code>PropertyChangeListener</code>s associated with
     *         the named property; if no such listeners have been added or if
     *         <code>propertyName</code> is <code>null</code>, an empty array is
     *         returned
     *
     * @see #addPropertyChangeListener(java.lang.String,
     *      java.beans.PropertyChangeListener)
     * @see #removePropertyChangeListener(java.lang.String,
     *      java.beans.PropertyChangeListener)
     * @see #getPropertyChangeListeners
     * @since 1.4
     */
    public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
        synchronized (getObjectLock()) {
            if (changeSupport == null) {
                return new PropertyChangeListener[0];
            }
            return changeSupport.getPropertyChangeListeners(propertyName);
        }
    }

    /**
     * Support for reporting bound property changes for Object properties. This
     * method can be called when a bound property has changed and it will send
     * the appropriate PropertyChangeEvent to any registered
     * PropertyChangeListeners.
     *
     * @param propertyName
     *            the property whose value has changed
     * @param oldValue
     *            the property's previous value
     * @param newValue
     *            the property's new value
     */
    protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        PropertyChangeSupport changeSupport;
        synchronized (getObjectLock()) {
            changeSupport = this.changeSupport;
        }
        if (changeSupport == null || (oldValue != null && newValue != null && oldValue.equals(newValue))) {
            return;
        }
        changeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

    /**
     * Support for reporting bound property changes for boolean properties. This
     * method can be called when a bound property has changed and it will send
     * the appropriate PropertyChangeEvent to any registered
     * PropertyChangeListeners.
     *
     * @param propertyName
     *            the property whose value has changed
     * @param oldValue
     *            the property's previous value
     * @param newValue
     *            the property's new value
     * @since 1.4
     */
    protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
        PropertyChangeSupport changeSupport = this.changeSupport;
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        changeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

    /**
     * Support for reporting bound property changes for integer properties. This
     * method can be called when a bound property has changed and it will send
     * the appropriate PropertyChangeEvent to any registered
     * PropertyChangeListeners.
     *
     * @param propertyName
     *            the property whose value has changed
     * @param oldValue
     *            the property's previous value
     * @param newValue
     *            the property's new value
     * @since 1.4
     */
    protected void firePropertyChange(String propertyName, int oldValue, int newValue) {
        PropertyChangeSupport changeSupport = this.changeSupport;
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        changeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

    /**
     * Reports a bound property change.
     *
     * @param propertyName
     *            the programmatic name of the property that was changed
     * @param oldValue
     *            the old value of the property (as a byte)
     * @param newValue
     *            the new value of the property (as a byte)
     * @see #firePropertyChange(java.lang.String, java.lang.Object,
     *      java.lang.Object)
     * @since 1.5
     */
    public void firePropertyChange(String propertyName, byte oldValue, byte newValue) {
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        firePropertyChange(propertyName, Byte.valueOf(oldValue), Byte.valueOf(newValue));
    }

    /**
     * Reports a bound property change.
     *
     * @param propertyName
     *            the programmatic name of the property that was changed
     * @param oldValue
     *            the old value of the property (as a char)
     * @param newValue
     *            the new value of the property (as a char)
     * @see #firePropertyChange(java.lang.String, java.lang.Object,
     *      java.lang.Object)
     * @since 1.5
     */
    public void firePropertyChange(String propertyName, char oldValue, char newValue) {
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        firePropertyChange(propertyName, new Character(oldValue), new Character(newValue));
    }

    /**
     * Reports a bound property change.
     *
     * @param propertyName
     *            the programmatic name of the property that was changed
     * @param oldValue
     *            the old value of the property (as a short)
     * @param newValue
     *            the old value of the property (as a short)
     * @see #firePropertyChange(java.lang.String, java.lang.Object,
     *      java.lang.Object)
     * @since 1.5
     */
    public void firePropertyChange(String propertyName, short oldValue, short newValue) {
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        firePropertyChange(propertyName, Short.valueOf(oldValue), Short.valueOf(newValue));
    }

    /**
     * Reports a bound property change.
     *
     * @param propertyName
     *            the programmatic name of the property that was changed
     * @param oldValue
     *            the old value of the property (as a long)
     * @param newValue
     *            the new value of the property (as a long)
     * @see #firePropertyChange(java.lang.String, java.lang.Object,
     *      java.lang.Object)
     * @since 1.5
     */
    public void firePropertyChange(String propertyName, long oldValue, long newValue) {
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        firePropertyChange(propertyName, Long.valueOf(oldValue), Long.valueOf(newValue));
    }

    /**
     * Reports a bound property change.
     *
     * @param propertyName
     *            the programmatic name of the property that was changed
     * @param oldValue
     *            the old value of the property (as a float)
     * @param newValue
     *            the new value of the property (as a float)
     * @see #firePropertyChange(java.lang.String, java.lang.Object,
     *      java.lang.Object)
     * @since 1.5
     */
    public void firePropertyChange(String propertyName, float oldValue, float newValue) {
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        firePropertyChange(propertyName, Float.valueOf(oldValue), Float.valueOf(newValue));
    }

    /**
     * Reports a bound property change.
     *
     * @param propertyName
     *            the programmatic name of the property that was changed
     * @param oldValue
     *            the old value of the property (as a double)
     * @param newValue
     *            the new value of the property (as a double)
     * @see #firePropertyChange(java.lang.String, java.lang.Object,
     *      java.lang.Object)
     * @since 1.5
     */
    public void firePropertyChange(String propertyName, double oldValue, double newValue) {
        if (changeSupport == null || oldValue == newValue) {
            return;
        }
        firePropertyChange(propertyName, Double.valueOf(oldValue), Double.valueOf(newValue));
    }

}
